﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Data;

namespace AcceptanceTest.Framework
{
    public class TestRunner
    {
        //public TestRunner(object instance)
        //{
        //    this.instance = instance;
        //}

        object instance;
        Dictionary<string, MethodInfo> stepDefinitions = new Dictionary<string, MethodInfo>();
        Dictionary<string, string> runOnceSteps;

        public event EventHandler<ExecutionMessage> OnStatusUpdated = delegate { };

        public void Run(Feature feature, DataSet dataSet)
        {
            runOnceSteps = new Dictionary<string, string>();

            //OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Begin, feature));

            if (dataSet == null || dataSet.Tables["settings"] == null)
            {
                OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Info, "Missing settings table"));
            }
            else
            {
                Config(instance, dataSet.Tables["settings"]);
            }         
            foreach (var scenario in feature.Scenarios)
            {
                Run(scenario, dataSet);
            }

            //OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.End, feature));
        }

        private void Config(object instance, DataTable dataTable)
        {

            Type type = instance.GetType();
            var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
            foreach (PropertyInfo property in properties)
            {
                var attrs = property.GetCustomAttributes(typeof(ConfigAttribute), true);
                if (attrs.Length > 0)
                {
                    string key = ((ConfigAttribute)attrs[0]).Key;
                    try
                    {
                        var val = (from row in dataTable.AsEnumerable()
                                   where string.Compare(key, row[0].ToString(), true) == 0
                                   select row[1]).FirstOrDefault();
                        property.SetValue(instance, Convert.ChangeType(val, property.PropertyType), null);

                    }
                    catch (StepExecutionException ex)
                    {
                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Failed, "Cannot configure: " + key, ex));
                    }
                }
            }
        }

        public void Run(Scenario scenario, DataSet dataSet)
        {
            var match = Regex.Match(scenario.Name, @"\[(.+)\]", RegexOptions.IgnoreCase);
            if (match.Success)
            {
                string tablename = match.Groups[1].Value;
                if (dataSet.Tables[tablename] == null)
                {
                    throw new StepDefinitionException("Cannot find scenario outline table: " + tablename);
                }

                OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Begin, tablename + "#0"));
                int n = 0;
                foreach (DataRow row in dataSet.Tables[tablename].Rows)
                {
                    n++;
                    string rowId = tablename + "#" + n;
                    try
                    {
                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Begin, scenario));
                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Begin, rowId));

                        for (int i = 0; i < scenario.Steps.Length; i++)
                        {
                            string step = scenario.Steps[i];

                            //if (!Regex.IsMatch(step, @"\<(.[^\>]+)\>")) //implicitly if no data template found, run once
                            if(step.Contains("!")) //explicitly to mark step run once
                            {
                                if (runOnceSteps.ContainsKey(step))
                                {
                                    continue;
                                }
                                else
                                {                                    
                                    runOnceSteps.Add(step, step);
                                }
                            }

                            string step2 = Regex.Replace(step, @"\<(.[^\>]+)\>",
                                (m) =>
                                {
                                    string colname = m.Groups[1].Value;
                                    if (row.Table.Columns[colname] == null)
                                    {
                                        throw new StepDefinitionException("Cannot find: <" + colname + "> in table: " + row.Table.TableName);
                                    }
                                    string v = row[colname].ToString();
                                    if (v.Contains('\n'))
                                    {
                                        v = v.Replace("\n", ";");
                                    }
                                    return v;
                                },
                                RegexOptions.IgnoreCase);

                            ExecuteStep(step2, scenario.Name + "." + step);
                        }

                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.End, scenario));
                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.End, rowId));
                    }
                    catch (StepExecutionException ex)
                    {
                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Failed, scenario, ex));
                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Failed, rowId, ex));
                    }
                    catch (Exception ex)
                    {
                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Error, scenario, ex));
                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Error, rowId, ex));
                    }
                }
                OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.End, tablename + "#0"));
            }
            else
            {
                try
                {
                    OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Begin, scenario));
                    foreach (var step in scenario.Steps)
                    {
                        ExecuteStep(step, scenario.Name + "." + step);
                    }
                    OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.End, scenario));
                }
                catch (StepExecutionException ex)
                {
                    OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Failed, scenario, ex));
                }
                catch (Exception ex)
                {
                    OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Error, scenario, ex));
                    throw;
                }
            }
        }

        public void LoadSteps(object instance)
        {
            this.instance = instance;
            Type type = instance.GetType();

            var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public);
            foreach (MethodInfo method in methods)
            {
                var attrs = method.GetCustomAttributes(typeof(StepAttribute), true);
                if (attrs.Length > 0)
                {
                    string title = ((StepAttribute)attrs[0]).Title;
                    if (stepDefinitions.ContainsKey(title))
                    {
                        throw new StepDefinitionException("Duplicated step definition: " + title);
                    }
                    stepDefinitions.Add(title, method);
                }
            }
        }
        
        private void ExecuteStep(string text)
        {
            ExecuteStep(text, text);
        }

        private void ExecuteStep(string text, string stepName)
        {
            OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Begin, stepName));

            MethodInfo method = null;
            foreach (var pattern in stepDefinitions.Keys)
            {
                var match = Regex.Match(text, pattern, RegexOptions.IgnoreCase);
                if(match.Success)
                {
                    object[] values = null;
                    try
                    {
                        method = stepDefinitions[pattern];
                        values = ParseParameters(method, match.Groups);
                    }
                    catch (Exception ex)
                    {
                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Error, stepName));

                        throw new StepDefinitionException(
                            string.Format("Cannot parse parameters from: {0}: {1}", text, ex.Message), ex);
                    }

                    try
                    {        
                        method.Invoke(instance, values);
                    }
                    catch (Exception ex)
                    {
                        OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.Failed, stepName));
                        
                        //because method.Invoke warps the real exception with TargetInvocationException
                        //we need message of inner exception
                        string msg = ex.InnerException != null ? ex.InnerException.Message : ex.Message;

                        //throw new StepExecutionException(
                        //    string.Format("Failed: {0}: {1}", text, msg), ex.InnerException??ex);

                        throw new StepExecutionException(msg, ex.InnerException ?? ex);
                    }
                }
            }

            if (method == null)
            {
                throw new StepDefinitionException(string.Format("Cannot find step: {0}", text));
            }

            OnStatusUpdated(this, new ExecutionMessage(ExecutionStatus.End, stepName));
        }

        private object[] ParseParameters(MethodInfo method, GroupCollection groupCollection)
        {
            var paraminfos = method.GetParameters();
            if (paraminfos.Length==0) return null;

            object[] values = new object[paraminfos.Length];
            for (int i = 0; i < paraminfos.Length; i++)
            {
                values[i] = Convert.ChangeType(groupCollection[i + 1].Value, paraminfos[i].ParameterType);
            }
            return values;
        }
    }

    public class ExecutionMessage : EventArgs
    {
        public ExecutionMessage(ExecutionStatus status, object tag)
        {
            this.Status = status;
            this.Tag = tag;
        }

        public ExecutionMessage(ExecutionStatus status, object tag, Exception ex)
        {
            this.Status = status;
            this.Tag = tag;
            this.Exception = ex;
        }

        public Exception Exception { get; set; }
        public ExecutionStatus Status { get; set; }
        public object Tag { get; set; }
    }

    public enum ExecutionStatus
    {
        Begin,
        End,
        Info,
        Failed,
        Error,
    }
}
